package com.googlecode.stemwijzer.rebind;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.PrintWriter;
import java.util.Date;

import com.google.gwt.core.ext.Generator;
import com.google.gwt.core.ext.GeneratorContext;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.core.ext.typeinfo.JClassType;
import com.google.gwt.core.ext.typeinfo.JMethod;
import com.google.gwt.core.ext.typeinfo.JParameter;
import com.google.gwt.core.ext.typeinfo.JParameterizedType;
import com.google.gwt.core.ext.typeinfo.NotFoundException;
import com.google.gwt.core.ext.typeinfo.TypeOracle;
import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
import com.google.gwt.user.rebind.SourceWriter;
import com.googlecode.stemwijzer.rebind.util.GeneratorDebugProxyPrintWriter;

/**
 * Base generator which you can implement and provides some default functionality.
 * 
 * @author pascal.janssen
 * 
 */
public abstract class BaseGenerator extends Generator {

	private static final String GENERATED_SOURCES_DIRECTORY = "generated_sources";

	/**
	 * The type of the generated class.
	 */
	protected JClassType generatedType;

	/**
	 * The name of the package the generated class will be put into.
	 */
	protected String packageName;

	/**
	 * The name of the generated class.
	 */
	protected String className;

	protected SourceWriter srcWriter;

	protected TypeOracle typeOracle;

	/**
	 * {@inheritDoc}
	 * 
	 * @see com.google.gwt.core.ext.Generator#generate(com.google.gwt.core.ext.TreeLogger,
	 *      com.google.gwt.core.ext.GeneratorContext, java.lang.String)
	 */
	@Override
	public String generate(final TreeLogger logger, final GeneratorContext context, final String typeName)
			throws UnableToCompleteException {

		this.typeOracle = context.getTypeOracle();

		// create output directory
		File outputDir = new File(GENERATED_SOURCES_DIRECTORY);
		if (!outputDir.exists() && !outputDir.mkdirs()) {
			logger.log(TreeLogger.ERROR, "Debug sources output directory could not be created");
		}
		logger.log(TreeLogger.DEBUG, "Debug sources output directory created");

		try {
			logger.log(TreeLogger.DEBUG, "Generating: " + typeName);

			this.generatedType = this.typeOracle.getType(typeName);
			this.packageName = this.generatedType.getPackage().getName();
			this.className = this.generatedType.getSimpleSourceName() + "GeneratedImpl";

			// create composer factory
			ClassSourceFileComposerFactory composer = new ClassSourceFileComposerFactory(packageName, className);
			onCompose(composer);

			// create print writer
			PrintWriter printWriter = context.tryCreate(logger, packageName, className);
			if (printWriter == null) {
				logger.log(TreeLogger.DEBUG, "Package and class already exists");
				return null;
			}

			// create debug file
			File debugFile = new File(outputDir, hashCode() + "_" + Long.toString(new Date().getTime()) + "_"
					+ this.className + ".java");

			GeneratorDebugProxyPrintWriter debugWriter = new GeneratorDebugProxyPrintWriter(printWriter, debugFile);

			// create source writer
			srcWriter = composer.createSourceWriter(context, debugWriter);
			if (srcWriter == null) {
				logger.log(TreeLogger.ERROR, "SourceWriter could not be created");
				throw new UnableToCompleteException();
			}

			onGenerate();

			srcWriter.outdent();
			srcWriter.println("}");
			debugWriter.close();

			context.commit(logger, printWriter);

			return composer.getCreatedClassName();

		} catch (NotFoundException nfe) {
			logger.log(TreeLogger.ERROR, "Unable to find required type(s)", nfe);
			throw new UnableToCompleteException();
		} catch (FileNotFoundException e) {
			logger.log(TreeLogger.ERROR, "File not found", e);
			throw new UnableToCompleteException();
		}
	}

	/**
	 * Gets the generic type of the implemented interface. If you for example implement interface
	 * "java.b.MyInterface<java.lang.String>", this method returns java.lang.String.
	 * 
	 * @param classThatImplementsTheInterface
	 *            - The class that implements the generic typed interface.
	 * @return - The generic typed interface.
	 * @throws UnableToCompleteException
	 *             - Thrown if no such generic typed interface is found.
	 */
	protected JClassType getGenericTypedInterface(final JClassType classThatImplementsTheInterface) {
		JClassType[] interfaces = classThatImplementsTheInterface.getImplementedInterfaces();

		if (interfaces.length == 1) {
			JClassType translatorInterface = interfaces[0];
			JParameterizedType type = translatorInterface.isParameterized();

			if (type != null) {
				JClassType[] genericTypes = type.getTypeArgs();

				if (genericTypes.length == 1) {
					return genericTypes[0];
				}
			}
		}
		return null;
	}

	/**
	 * Extracts the name of the property from the method.
	 * 
	 * @param method
	 *            - A getter or setter method.
	 * 
	 * @return - Property name (eg, methodname is "getColor" --> returns "color");
	 */
	protected String getPropertyName(final JMethod method) {
		return method.getName().substring(3, 4).toLowerCase() + method.getName().substring(4);
	}

	/**
	 * Returns the setter argument as final.
	 * 
	 * @param param
	 *            - The parameter.
	 * @return - "final java.util.Date date"
	 */
	protected String getSetterArgument(final JParameter param) {
		return "final " + param.getType().getQualifiedSourceName() + " " + param.getName();
	}

	/**
	 * This is the only place you have a composer to do stuff with (on class level). For instance implement an
	 * interface.
	 * 
	 * @param composer
	 *            - The composer.
	 */
	protected abstract void onCompose(ClassSourceFileComposerFactory composer);

	/**
	 * Implement this method to do generator specific stuff (generate your class!).
	 * 
	 * @throws UnableToCompleteException
	 */
	protected abstract void onGenerate() throws UnableToCompleteException;

	/**
	 * Writes "@Override".
	 */
	private void writeAnnotationOverride() {
		srcWriter.println("@Override");
	}

	/**
	 * Outdents and writes "}".
	 */
	protected void writeCloseCodeBlock() {
		srcWriter.outdent();
		srcWriter.println("}");
	}

	/**
	 * Writes close code block and prints line.
	 */
	protected void writeCloseMethod() {
		writeCloseCodeBlock();
		srcWriter.println();
	}

	/**
	 * Writes an empty constructor of the class with the given modifier (private, public etc).
	 * 
	 * @param visibility
	 *            - The visibility modifier.
	 */
	protected void writeConstructor(final String visibility) {
		srcWriter.println(visibility + " " + this.className + "() {}");
		srcWriter.println();
	}

	/**
	 * Writes a method header eg "public final void myCoolMethod(String param1, String param2) {".
	 * 
	 * @param methodName
	 *            - The name of the method
	 * @param returnType
	 *            - The return type ("void" or an actual return type)
	 * @param argument
	 *            - The argument (null or an actual argument "String param1, String param2")
	 */
	protected void writeMethodHeader(final String methodName, final String returnType, final String argument) {
		srcWriter.print("public final " + returnType + " " + methodName + "(");

		if (argument != null) {
			srcWriter.print(argument);
		}

		srcWriter.println(") {");
		srcWriter.indent();
	}

	/**
	 * Writes a method header with annotation Override eg "public final void myCoolMethod(String param1, String param2)
	 * {".
	 * 
	 * @param methodName
	 *            - The name of the method
	 * @param returnType
	 *            - The return type ("void" or an actual return type)
	 * @param argument
	 *            - The argument (null or an actual argument "String param1, String param2")
	 */
	protected void writeOverriddenMethodHeader(final String methodName, final String returnType, final String argument) {
		this.writeAnnotationOverride();
		this.writeMethodHeader(methodName, returnType, argument);
	}

	/**
	 * Writes method header with annotation Override for property method (setter or getter) eg
	 * "public final java.lang.String getLorumIpsum() {".
	 * 
	 * @param method
	 *            - The getter or setter to write method header for.
	 */
	protected void writeOverriddenPropertyMethodHeader(final JMethod method) {
		this.writeAnnotationOverride();
		this.writePropertyMethodHeader(method);
	}

	/**
	 * Writes instance property eg "private java.lang.String myProperty".
	 * 
	 * @param name
	 *            - The name of the property.
	 * @param type
	 *            - The type of the property.
	 */
	protected void writeProperty(final String name, final String type) {
		srcWriter.println("private " + type + " " + name + ";");
	}

	/**
	 * Writes method header for property method (setter or getter) eg "public final java.lang.String getLorumIpsum() {".
	 * 
	 * @param method
	 *            - The getter or setter to write method header for.
	 */
	protected void writePropertyMethodHeader(final JMethod method) {
		String methodName = method.getName();
		if (methodName.startsWith("get")) {
			String type = method.getReturnType().getQualifiedSourceName();
			this.writeMethodHeader(methodName, type, null);
		} else {
			JParameter setterParam = method.getParameters()[0];
			this.writeMethodHeader(methodName, "void", getSetterArgument(setterParam));
		}
	}

	protected void writeComment(final String comment) {
		srcWriter.println("// " + comment);
	}
}
